Skip to content

Conversation

@amazon-q-developer
Copy link

Implements comprehensive VM snapshot functionality that captures and restores complete runtime state for time-travel debugging.

New Features:

  • vm.takeSnapshot() - Captures complete VM state including sprite positions, variables, running threads, timers, and IO device state
  • vm.loadSnapshot(snapshotData) - Restores VM to exact previous state
  • Emits 'SNAPSHOT_LOADED' event when restoration completes

Implementation:

  • Created src/serialization/snapshot.js module for state serialization/deserialization
  • Reuses existing SB3 serialization for project structure (blocks, costumes, sounds)
  • Captures runtime state: target positions/effects, variable values, monitor state, thread execution stacks, timers, IO device state
  • Includes validation, error handling, and version tracking for future compatibility

Use Cases:

  • Time-travel debugging: Save execution points and restore to debug issues
  • Project state management: Create save points during development
  • Educational tools: Allow students to experiment and revert changes

The snapshot format is designed to be comprehensive yet efficient, capturing all state needed to restore the VM to an exact execution point while reusing proven serialization code for static project data.


Code References

The following code references were used in generating these changes:

@amazon-q-developer
Copy link
Author

Resolves Issue #26

@amazon-q-developer amazon-q-developer bot mentioned this pull request Jan 2, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 2, 2026

Summary by CodeRabbit

  • New Features
    • Added snapshot functionality allowing users to save and restore complete VM runtime state
    • Captured state includes targets, variables, lists, monitors, execution threads, and I/O device configuration
    • Enables saving program state at any point and resuming from saved snapshots
    • Includes validation and error handling to ensure reliable state restoration

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

A new snapshot serialization module captures and restores complete VM runtime state including project data, targets, monitors, threads, timers, and IO devices. The VirtualMachine class gains takeSnapshot() and loadSnapshot() methods to serialize and deserialize snapshots respectively.

Changes

Cohort / File(s) Summary
Snapshot Serialization Module
src/serialization/snapshot.js
New file introducing serialize(runtime) and deserialize(runtime, snapshot) functions. Serialization combines SB3 project data with runtime state (targets with positions/variables/lists, monitors, threads with stacks/frames/warp state, IO devices, and timing data). Deserialization validates snapshot format, restores project structure, then sequentially restores targets, monitors, threads, timers, and IO devices with defensive handling for missing components.
Virtual Machine Snapshot Methods
src/virtual-machine.js
Added takeSnapshot() method that returns serialized VM runtime state. Added loadSnapshot(snapshotData) method that validates input, halts VM activity, deserializes snapshot into runtime, emits SNAPSHOT_LOADED event on success, and logs errors on failure.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant VM as VirtualMachine
    participant Snapshot as snapshot.js
    participant Targets
    participant Monitors
    participant Threads
    participant IODevices
    participant SB3 as sb3 module

    rect rgb(240, 248, 255)
    Note over User,SB3: Serialization Flow
    User->>VM: takeSnapshot()
    VM->>Snapshot: serialize(runtime)
    Snapshot->>SB3: sb3.serialize()
    SB3-->>Snapshot: project data
    Snapshot->>Targets: collect state
    Targets-->>Snapshot: positions, variables
    Snapshot->>Monitors: collect state
    Monitors-->>Snapshot: monitor values
    Snapshot->>Threads: collect state
    Threads-->>Snapshot: stacks, frames, warp state
    Snapshot->>IODevices: collect state
    IODevices-->>Snapshot: timers, keyboard, mouse, video
    Snapshot-->>VM: { project, runtimeState }
    VM-->>User: snapshot
    end

    rect rgb(240, 255, 240)
    Note over User,SB3: Deserialization Flow
    User->>VM: loadSnapshot(snapshotData)
    VM->>Snapshot: deserialize(runtime, snapshot)
    Snapshot->>Snapshot: validate format
    Snapshot->>SB3: sb3.deserialize()
    SB3-->>Snapshot: runtime loaded
    Snapshot->>Targets: restoreTargetRuntimeState()
    Snapshot->>Monitors: restoreMonitorState()
    Snapshot->>Threads: restoreThreadState()
    Snapshot->>IODevices: restoreIODeviceState()
    Snapshot-->>VM: restore complete
    VM->>VM: emit SNAPSHOT_LOADED
    VM-->>User: success
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • VM Snapshots #26: Directly addresses the same objectives—introduces snapshot serialization module with serialize/deserialize functions and VirtualMachine methods takeSnapshot/loadSnapshot to capture and restore project plus runtime state (targets, monitors, threads, timers, IO devices, timing information).

Poem

🐰 A snapshot in time, we now preserve,
Each target, thread, and timer's curve,
Save the moment, restore it whole,
The VM's soul in one small scroll! ⏰✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary change: adding VM snapshot functionality for time-travel debugging, which matches the main additions of takeSnapshot() and loadSnapshot() methods.
Description check ✅ Passed The description is directly related to the changeset, detailing the snapshot functionality, new methods, module creation, and implementation approach that correspond with the actual changes made.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@supervoidcoder
Copy link
Member

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@supervoidcoder
Copy link
Member

@8to16 omg look it doesn't hallucinate no more

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
src/serialization/snapshot.js (4)

156-197: Add error handling for individual restoration steps.

The deserialization process calls multiple restoration helpers (lines 172, 177, 182, 187, 192) in sequence without individual error handling. If any helper throws an exception, the entire snapshot load fails, which may be too strict.

Consider whether partial restoration should be allowed if certain components fail (e.g., thread state restoration fails but target positions should still be restored). Here's a more resilient approach:

🔎 Proposed enhancement for error resilience
 return sb3.deserialize(snapshot.project, runtime)
     .then(({targets, extensions}) => {
         // Restore target runtime state
         if (snapshot.runtimeState.targets) {
-            restoreTargetRuntimeState(runtime, snapshot.runtimeState.targets);
+            try {
+                restoreTargetRuntimeState(runtime, snapshot.runtimeState.targets);
+            } catch (e) {
+                log.error('Failed to restore target runtime state:', e);
+            }
         }
         
         // Restore monitor state
         if (snapshot.runtimeState.monitors) {
-            restoreMonitorState(runtime, snapshot.runtimeState.monitors);
+            try {
+                restoreMonitorState(runtime, snapshot.runtimeState.monitors);
+            } catch (e) {
+                log.error('Failed to restore monitor state:', e);
+            }
         }
         
         // Similar try-catch for other restoration steps...
         
         return {targets, extensions};
     });

Alternatively, if complete restoration is required, document that partial failures are not acceptable and ensure all helpers validate their inputs defensively.


204-247: Minor: Improve validation for position and costume restoration.

Two small improvements for defensive coding:

  1. Line 213: The setXY call checks if targetState.x is defined but uses both x and y. If x is defined but y is not (unusual but possible), this could cause unexpected behavior.

  2. Line 217: setCostume is called with targetState.currentCostume without validating that the index is within bounds for the target's costume list.

🔎 Proposed defensive improvements
-        if (typeof targetState.x !== 'undefined') target.setXY(targetState.x, targetState.y);
+        if (typeof targetState.x !== 'undefined' && typeof targetState.y !== 'undefined') {
+            target.setXY(targetState.x, targetState.y);
+        }

-        if (typeof targetState.currentCostume !== 'undefined') target.setCostume(targetState.currentCostume);
+        if (typeof targetState.currentCostume !== 'undefined') {
+            const maxCostume = target.getCostumes().length - 1;
+            const costumeIndex = Math.max(0, Math.min(targetState.currentCostume, maxCostume));
+            target.setCostume(costumeIndex);
+        }

These changes are optional but would make restoration more robust against malformed or outdated snapshots.


332-344: Consider time drift implications for timer restoration.

The timer restoration (lines 332-344) directly restores timing values including currentMSecs, currentStepTime, and projectTimer. If a snapshot is loaded minutes or hours after creation, these absolute time values may cause unexpected behavior.

For time-travel debugging, consider whether you want:

  1. Absolute time restoration: Keep current implementation (timers restore to snapshot values).
  2. Relative time restoration: Adjust timers relative to current time (e.g., reset projectTimer to 0).
  3. Hybrid approach: Reset some timers (project timer) but preserve others (step timing for animation consistency).

Document the intended behavior for timer restoration, as it affects debugging workflows (e.g., "go back 30 seconds" vs. "restore exact timer state").


351-374: Document input device state restoration behavior.

Restoring keyboard and mouse state (lines 352-366) may create mismatches between physical hardware state and the VM's restored state:

  • Keyboard: If keys were pressed in the snapshot but released physically, the VM thinks they're still pressed.
  • Mouse: Restored position may not match the actual cursor position.

Consider documenting this behavior or offering options:

  1. Full restoration (current): Restore all input state exactly (good for true time-travel debugging).
  2. Partial restoration: Skip keyboard/mouse state and only restore video state (better for "load checkpoint" use cases where user expects current input state).
  3. Reset on first input: Restore snapshot state but clear it on first real user input to resync.

The current approach is reasonable for time-travel debugging, but users should understand that phantom key presses or mouse positions from the snapshot may occur until they interact with the project again.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb00bb6 and f7b4915.

📒 Files selected for processing (2)
  • src/serialization/snapshot.js
  • src/virtual-machine.js
🧰 Additional context used
🧬 Code graph analysis (2)
src/virtual-machine.js (1)
src/serialization/snapshot.js (1)
  • log (10-10)
src/serialization/snapshot.js (1)
src/virtual-machine.js (7)
  • require (20-20)
  • require (24-24)
  • require (25-25)
  • require (26-26)
  • require (28-28)
  • Variable (21-21)
  • log (13-13)
🔇 Additional comments (2)
src/serialization/snapshot.js (2)

17-61: Variable type constants are properly defined and correctly used.

The Variable.SCALAR_TYPE and Variable.LIST_TYPE constants are defined as static getters in src/engine/variable.js (returning '' and 'list' respectively) and match the usage in the serialize function. The defensive handling for list values at lines 57–58 with Array.isArray() check is appropriate.


275-325: Snapshot infrastructure and APIs are already well-established.

The concern about "tight coupling to Thread internals" is overstated. The APIs used here are:

  • runtime._pushThread: Documented JSDoc method (runtime.js line 2083) used in tests and extensions
  • Thread._StackFrame.create() and .release(): Intentionally exported as Thread._StackFrame with comment "for extensions" (thread.js line 530), used in the codebase

The snapshot format already has:

  • Version management (version '1.0.0', checked during restore)
  • Comprehensive test infrastructure (test/snapshot/lib.js, test/integration/tw-snapshots.js)
  • Proper object pooling usage (correctly calls release() before creating new frames)

No changes needed; snapshot restoration is properly tested and managed.

Comment on lines +63 to +142
// Capture monitor state
if (runtime._monitorState) {
runtimeState.monitors = runtime._monitorState.valueSeq().toArray().map(monitor => ({
id: monitor.get('id'),
value: monitor.get('value'),
visible: monitor.get('visible'),
mode: monitor.get('mode'),
x: monitor.get('x'),
y: monitor.get('y')
}));
}

// Capture thread execution state
runtimeState.threads = runtime.threads.map(thread => ({
topBlock: thread.topBlock,
stack: [...thread.stack],
stackFrames: thread.stackFrames.map(frame => ({
warpMode: frame.warpMode,
isLoop: frame.isLoop,
reporting: frame.reporting,
reported: frame.reported,
waitingReporter: frame.waitingReporter,
params: frame.params ? Object.assign({}, frame.params) : null,
executionContext: frame.executionContext ? Object.assign({}, frame.executionContext) : null
})),
status: thread.status,
targetId: thread.target ? thread.target.id : null,
stackClick: thread.stackClick,
updateMonitor: thread.updateMonitor,
warpTimer: thread.warpTimer ? {
startTime: thread.warpTimer.startTime,
timeElapsed: thread.warpTimer.timeElapsed()
} : null
}));

// Capture runtime timing state
runtimeState.timers = {
currentMSecs: runtime.currentMSecs,
currentStepTime: runtime.currentStepTime,
_lastStepTime: runtime._lastStepTime
};

// Capture clock IO device state
if (runtime.ioDevices && runtime.ioDevices.clock) {
runtimeState.timers.projectTimer = runtime.ioDevices.clock._projectTimer;
runtimeState.timers.paused = runtime.ioDevices.clock._paused;
runtimeState.timers.pausedTime = runtime.ioDevices.clock._pausedTime;
}

// Capture IO device state
runtimeState.ioDevices = {};

// Capture keyboard state
if (runtime.ioDevices && runtime.ioDevices.keyboard) {
runtimeState.ioDevices.keyboard = {
keysPressed: [...runtime.ioDevices.keyboard._keysPressed],
lastKeyPressed: runtime.ioDevices.keyboard.lastKeyPressed
};
}

// Capture mouse state
if (runtime.ioDevices && runtime.ioDevices.mouse) {
runtimeState.ioDevices.mouse = {
clientX: runtime.ioDevices.mouse._clientX,
clientY: runtime.ioDevices.mouse._clientY,
scratchX: runtime.ioDevices.mouse._scratchX,
scratchY: runtime.ioDevices.mouse._scratchY,
isDown: runtime.ioDevices.mouse._isDown,
buttons: Array.from(runtime.ioDevices.mouse._buttons || [])
};
}

// Capture video state
if (runtime.ioDevices && runtime.ioDevices.video) {
runtimeState.ioDevices.video = {
skinId: runtime.ioDevices.video._skinId,
drawable: runtime.ioDevices.video._drawable,
ghost: runtime.ioDevices.video._ghost
};
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Significant coupling to private implementation details.

The serialize function extensively accesses underscore-prefixed private properties across multiple components:

  • Line 64: runtime._monitorState (private)
  • Lines 107-108: clock._projectTimer, _paused, _pausedTime (private)
  • Lines 118-119: keyboard._keysPressed, lastKeyPressed (private)
  • Lines 126-131: mouse._clientX, _clientY, _scratchX, _scratchY, _isDown, _buttons (private)
  • Lines 138-140: video._skinId, _drawable, _ghost (private)

This creates tight coupling to internal implementation details that may change without notice, breaking snapshot compatibility.

Consider:

  1. Public APIs: Where possible, use public getter methods instead of private properties.
  2. Documentation: Document which private APIs are dependencies for snapshot format.
  3. Versioning: The version field at line 23 is good, but consider a compatibility checking mechanism for handling format changes.
  4. Abstraction: Consider adding explicit serialization methods to IO devices (e.g., device.getState() / device.setState()).

The current approach is pragmatic for an initial implementation, but plan for a more robust solution if this feature is heavily used or needs long-term snapshot compatibility.

🤖 Prompt for AI Agents
In src/serialization/snapshot.js around lines 63 to 142 the serializer directly
reads underscore-prefixed private properties from runtime monitors, clock,
keyboard, mouse and video which couples snapshots to internal implementation;
replace these direct property accesses with calls to public APIs (use existing
getters or add and call new device.getState()/setState() or
monitor.serialize()/deserialize() methods), add safe fallbacks when a public
getter is not present (e.g., call the getter if available, otherwise skip or
log), and update the snapshot format documentation/versioning and add a
compatibility check so future internal changes don't break snapshot loading.

Comment on lines +254 to +268
const restoreMonitorState = function (runtime, monitorStates) {
monitorStates.forEach(monitorState => {
// Update monitor using runtime's monitor update system
const monitorRecord = new Map([
['id', monitorState.id],
['value', monitorState.value],
['visible', monitorState.visible],
['mode', monitorState.mode],
['x', monitorState.x],
['y', monitorState.y]
]);

runtime.requestUpdateMonitor(monitorRecord);
});
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for requestUpdateMonitor definition and usage
rg -n 'requestUpdateMonitor' --type=js -B 3 -A 10

Repository: OmniBlocks/scratch-vm

Length of output: 10276


🏁 Script executed:

# Also search for the serialization context - look for monitor-related code
rg -n 'monitorState|restoreMonitorState' --type=js -B 2 -A 5

Repository: OmniBlocks/scratch-vm

Length of output: 49475


🏁 Script executed:

# Check if there's any Immutable.js usage pattern for Map objects
rg -n 'Immutable\.Map|new Map' src/serialization/snapshot.js -B 2 -A 2

Repository: OmniBlocks/scratch-vm

Length of output: 312


🏁 Script executed:

# Check the imports at the top of snapshot.js
head -50 src/serialization/snapshot.js

Repository: OmniBlocks/scratch-vm

Length of output: 1657


🏁 Script executed:

# Also verify the exact API that requestUpdateMonitor expects by looking at its implementation more closely
sed -n '3186,3210p' src/engine/runtime.js

Repository: OmniBlocks/scratch-vm

Length of output: 1197


🏁 Script executed:

# Check what Map is being used in the runtime.js file
rg -n "^const.*Map|^const.*Immutable|import.*Map" src/engine/runtime.js | head -20

Repository: OmniBlocks/scratch-vm

Length of output: 145


🏁 Script executed:

# Check if MonitorRecord is defined and what it is
rg -n "MonitorRecord" src/engine/runtime.js -B 2 -A 2 | head -30

Repository: OmniBlocks/scratch-vm

Length of output: 343


🏁 Script executed:

# Check if snapshot.js imports Immutable or Map from anywhere
rg -n "require.*immutable|import.*immutable|Map" src/serialization/snapshot.js | head -20

Repository: OmniBlocks/scratch-vm

Length of output: 143


🏁 Script executed:

# Look at how other files in the serialization module handle Maps
rg -n "new Map\|Map({" src/serialization/*.js

Repository: OmniBlocks/scratch-vm

Length of output: 167


🏁 Script executed:

# Search for Map usage in serialization files more carefully
rg -n "Map(" src/serialization/

Repository: OmniBlocks/scratch-vm

Length of output: 123


🏁 Script executed:

# Also check sb2.js to see how it creates monitor records
rg -n "MonitorRecord" src/serialization/sb2.js -B 2 -A 5

Repository: OmniBlocks/scratch-vm

Length of output: 849


🏁 Script executed:

# Check what MonitorRecord is and how it's defined
cat src/engine/monitor-record.js

Repository: OmniBlocks/scratch-vm

Length of output: 678


🏁 Script executed:

# Also check how execute.js and blocks.js create Maps for requestUpdateMonitor
rg -n "requestUpdateMonitor" src/engine/execute.js -B 2 -A 8 | head -30

Repository: OmniBlocks/scratch-vm

Length of output: 523


🏁 Script executed:

# Check if snapshot.js imports anything from immutable
cat src/serialization/snapshot.js | grep -E "require|import" | head -20

Repository: OmniBlocks/scratch-vm

Length of output: 278


🏁 Script executed:

# Check how runtime.js defines and imports Map
rg -n "Map" src/engine/runtime.js | head -20

Repository: OmniBlocks/scratch-vm

Length of output: 1331


🏁 Script executed:

# Check if Immutable is imported anywhere in execute.js to see how Map is used there
head -30 src/engine/execute.js | grep -E "require|import|Map"

Repository: OmniBlocks/scratch-vm

Length of output: 321


🏁 Script executed:

# Verify the exact usage in execute.js
sed -n '1,10p' src/engine/execute.js

Repository: OmniBlocks/scratch-vm

Length of output: 411


Import Map from immutable and use Map({...}) syntax.

At line 257, the code creates a native JavaScript Map, but requestUpdateMonitor expects an Immutable.js Map. The runtime will fail when calling .mergeWith() on line 3198 of runtime.js since native Map lacks this method.

Change snapshot.js to:

  1. Add import at the top: const {Map} = require('immutable');
  2. Replace new Map([...]) with Map({...}) syntax to match the pattern used in execute.js (line 103), blocks.js (line 714), and target.js (line 355).
Example fix
const {Map} = require('immutable');

// In restoreMonitorState function:
const monitorRecord = Map({
    id: monitorState.id,
    value: monitorState.value,
    visible: monitorState.visible,
    mode: monitorState.mode,
    x: monitorState.x,
    y: monitorState.y
});
🤖 Prompt for AI Agents
In src/serialization/snapshot.js around lines 254 to 268, the code builds a
native JS Map but runtime.requestUpdateMonitor expects an Immutable.js Map;
import Immutable's Map at the top (const {Map} = require('immutable');) and
replace new Map([...]) with the Immutable Map({...}) object-literal factory form
for monitorRecord so requestUpdateMonitor receives an Immutable Map with the
expected methods.

Comment on lines +1921 to +1930
/**
* Take a snapshot of the current VM state for time-travel debugging.
* Captures complete runtime state including sprite positions, variables,
* running threads, timers, and IO device state.
* @return {object} Snapshot object containing all VM state.
*/
takeSnapshot () {
const snapshot = require('./serialization/snapshot');
return snapshot.serialize(this.runtime);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, verify the actual code at the specified lines
cat -n src/virtual-machine.js | sed -n '1920,1935p'

Repository: OmniBlocks/scratch-vm

Length of output: 805


🏁 Script executed:

# Search for takeSnapshot usage in the codebase
rg -n 'takeSnapshot' --type=js

Repository: OmniBlocks/scratch-vm

Length of output: 114


🏁 Script executed:

# Look for performance considerations or async patterns with serialization
rg -n -B2 -A2 'serialize|snapshot' src/virtual-machine.js | head -50

Repository: OmniBlocks/scratch-vm

Length of output: 2367


🏁 Script executed:

# Check if there are performance tests related to snapshots
fd -t f '.*test.*' -o '.*spec.*' | xargs rg -l 'snapshot|serialize' 2>/dev/null || echo "Checking test files..."

Repository: OmniBlocks/scratch-vm

Length of output: 2221


🏁 Script executed:

# Search for calls to takeSnapshot (not just the definition)
rg -n 'takeSnapshot\s*\(' --type=js | grep -v '^src/virtual-machine.js:1927'

Repository: OmniBlocks/scratch-vm

Length of output: 47


🏁 Script executed:

# Examine the snapshot serializer implementation
cat -n src/serialization/snapshot.js | head -80

Repository: OmniBlocks/scratch-vm

Length of output: 3406


🏁 Script executed:

# Check the test file for snapshot behavior
cat test/integration/tw-snapshots.js

Repository: OmniBlocks/scratch-vm

Length of output: 1078


🏁 Script executed:

# Look for sb3.serialize implementation and any performance notes
rg -n -B3 -A3 'sb3.serialize' src/serialization/snapshot.js

Repository: OmniBlocks/scratch-vm

Length of output: 307


🏁 Script executed:

# Check if there are async versions of serialization
rg -n 'serializeAsync|serialize.*async' --type=js

Repository: OmniBlocks/scratch-vm

Length of output: 168


🏁 Script executed:

# Look for any warnings or TODOs about synchronous operations
rg -n 'TODO|FIXME|performance|sync.*block|large.*project' src/virtual-machine.js | grep -i snapshot

Repository: OmniBlocks/scratch-vm

Length of output: 47


🏁 Script executed:

# Check what sb3.serialize actually does
rg -n 'const serialize.*=' src/serialization/sb3.js | head -5

Repository: OmniBlocks/scratch-vm

Length of output: 329


🏁 Script executed:

# Check how large projects might be serialized elsewhere and any caching
rg -n 'saveProject|exportProject' --type=js -A5 | grep -E '(saveProject|exportProject|serialize|async)' | head -20

Repository: OmniBlocks/scratch-vm

Length of output: 1889


🏁 Script executed:

# Check if there are performance measurements for sb3 serialization
rg -n 'performance|benchmark|measure|duration' src/serialization/sb3.js | head -10

Repository: OmniBlocks/scratch-vm

Length of output: 47


Consider async serialization or add performance guardrails for takeSnapshot().

The implementation correctly delegates to the snapshot serializer, but the synchronous call to sb3.serialize() for large projects could block the UI during time-travel debugging. While the method is currently unused, if it becomes actively used, measure snapshot performance and consider an async variant or background workers to prevent responsiveness issues.

🤖 Prompt for AI Agents
In src/virtual-machine.js around lines 1921 to 1930, the current takeSnapshot()
performs synchronous serialization which can block the UI for large projects;
change the API and implementation to provide a non-blocking path: add an async
takeSnapshotAsync() that returns a Promise and offloads work to a background
task (use a Worker thread / web worker or schedule via
setImmediate/queueMicrotask and chunked/streaming serialization), keep a thin
synchronous wrapper that either delegates to the async function
(throwing/deprecating it) or returns a quick shallow snapshot, and add simple
performance guardrails such as a configurable size/time threshold that aborts
serialization and logs/metrics the event so callers can fall back to lighter
snapshots. Ensure callers are updated or that both sync and async variants
coexist with deprecation notes.

Comment on lines +1938 to +1958
loadSnapshot (snapshotData) {
if (!snapshotData) {
return Promise.reject(new Error('No snapshot data provided'));
}

// Stop all current execution
this.stopAll();

const snapshot = require('./serialization/snapshot');

return snapshot.deserialize(this.runtime, snapshotData)
.then(({targets, extensions}) => {
// Emit event to notify that snapshot has been loaded
this.emit('SNAPSHOT_LOADED', snapshotData);
return {targets, extensions};
})
.catch(error => {
log.error('Failed to load snapshot:', error);
return Promise.reject(error);
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Missing VM state management and target installation workflow.

The loadSnapshot() method calls stopAll() but doesn't call clear() to remove existing targets before restoration. Additionally, it doesn't follow the established pattern used by deserializeProject() for properly integrating restored targets into the VM:

  1. Missing clear() call: Line 1944 stops execution but doesn't dispose of existing targets, which may interfere with snapshot restoration.
  2. Missing installTargets() call: The method doesn't call installTargets() to properly integrate the restored targets, load extensions, and set the editing target.
  3. Missing UI updates: No calls to emitTargetsUpdate() or emitWorkspaceUpdate() to notify the UI.
  4. Unused promise resolution: The {targets, extensions} returned from deserialize() at line 1952 is not used.

Compare with deserializeProject() (lines 732-771) which provides the correct pattern:

🔎 Proposed fix to follow the deserializeProject pattern
 loadSnapshot (snapshotData) {
     if (!snapshotData) {
         return Promise.reject(new Error('No snapshot data provided'));
     }

-    // Stop all current execution
-    this.stopAll();
+    // Clear the current runtime (stops execution and disposes targets)
+    this.clear();

     const snapshot = require('./serialization/snapshot');
     
     return snapshot.deserialize(this.runtime, snapshotData)
         .then(({targets, extensions}) => {
-            // Emit event to notify that snapshot has been loaded
-            this.emit('SNAPSHOT_LOADED', snapshotData);
-            return {targets, extensions};
+            return this.installTargets(targets, extensions, true);
+        })
+        .then(() => {
+            // Emit event to notify that snapshot has been loaded
+            this.emit('SNAPSHOT_LOADED', snapshotData);
         })
         .catch(error => {
             log.error('Failed to load snapshot:', error);
             return Promise.reject(error);
         });
 }

Committable suggestion skipped: line range outside the PR's diff.

@ampelectrecuted

This comment has been minimized.

@coderabbitai
Copy link

coderabbitai bot commented Jan 3, 2026

Tip

For best results, initiate chat on the files or code changes.

@8to16 Thank you for the positive feedback! I'm glad to hear the reviews have been helpful. I strive to provide accurate, context-aware analysis based on the actual code and project structure.

If you'd like me to review PR #29 or if there's anything specific you'd like me to look at, feel free to let me know! 🚀

@ampelectrecuted

This comment has been minimized.

@coderabbitai
Copy link

coderabbitai bot commented Jan 3, 2026

Tip

For best results, initiate chat on the files or code changes.

@8to16 Ah, got it! 😅 My mistake - I misunderstood the context. You're right, looking at this PR from Amazon Q, the description is focused and relevant without unnecessary mentions of internationalization. That's definitely an improvement in PR quality from the bot!

Thanks for clarifying!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants